%%%-------------------------------------------------------------------
%%% Licensed to the Apache Software Foundation (ASF) under one
%%% or more contributor license agreements.  See the NOTICE file
%%% distributed with this work for additional information
%%% regarding copyright ownership.  The ASF licenses this file
%%% to you under the Apache License, Version 2.0 (the
%%% "License"); you may not use this file except in compliance
%%% with the License.  You may obtain a copy of the License at
%%%
%%%   http://www.apache.org/licenses/LICENSE-2.0
%%%
%%% Unless required by applicable law or agreed to in writing,
%%% software distributed under the License is distributed on an
%%% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
%%% KIND, either express or implied.  See the License for the
%%% specific language governing permissions and limitations
%%% under the License.
%%%

-module(grpc_lib_compile).

-export([file/2]).

%%% ============================================================================
%%% Exported functions
%%% ============================================================================

file(Filename, Options) ->
    {Generate, OtherOptions} = grpc_lib:keytake(generate, Options, server),
    GPBModule = compile_pb(Filename, OtherOptions),
    case Generate of 
        client ->
            write_client(GPBModule);
        server ->
            write_server(GPBModule)
    end.

%%% ============================================================================
%%% Internal functions
%%% ============================================================================

compile_pb(Filename, Options) ->
    %ok = gpb_compile:file(Filename, [maps | Options ++ [{i, "."}]]),
    _CompiledPB =  output_compile_pb(Filename, Options),
    % GpbInludeDir = filename:join(code:lib_dir(gpb), "include"),
    % {ok, Module, Compiled} = compile:file(CompiledPB, 
    %                                       [binary, {i, GpbInludeDir}]),
    % {module, _} = code:load_binary(Module, CompiledPB, Compiled),
    % Module.
    invalid.

output_compile_pb (Filename, Options) ->
    Folder = proplists:get_value(o,Options,filename:dirname(Filename)),
    filename:join([Folder,filename:rootname(filename:basename(Filename)) ++ ".erl"]).

write_server(Module) ->
    ModuleName = atom_to_list(Module) ++ "_server",
    Header = print_header(ModuleName),
    Exports = print_server_exports(Module),
    Messages = print_messages(Module),
    Decoder = print_decoder(Module),
    Services = print_services(Module, server),
    ok = file:write_file(ModuleName ++ ".erl", [Header, Exports, Messages,
                                                Decoder, Services]).

write_client(Module) ->
    ModuleName = atom_to_list(Module) ++ "_client",
    Header = print_header(ModuleName),
    Exports = print_client_exports(Module),
    TypeExports = print_type_exports(Module),
    Messages = print_messages(Module),
    Decoder = print_decoder(Module),
    Services = print_services(Module, client),
    ok = file:write_file(ModuleName ++ ".erl", [Header, Exports, Messages,
                                                TypeExports, Decoder,
                                                Services]).

print_header(ModuleName) ->
    ["-module(", string_to_atom(ModuleName), ").\n\n"
     "%% this file was generated by grpc\n\n"].

print_server_exports(Module) ->
    RPCs = rpcs(Module),
    to_io_list(["-export([",
                separate(["decoder/0"] ++
                          [[Name, "/3"]|| #{name := Name} <- RPCs], ",\n         "),
                "]).\n\n"]).

%% Only "simple rpcs" (no streaming) are considered.
print_client_exports(Module) ->
    RPCs = rpcs(Module),
    to_io_list(["-export([",
                separate([[Name, "/3"]|| #{name := Name,
                                           output_stream := false,
                                           input_stream := false} <- RPCs], ",\n         "),
                "]).\n\n"]).

%% Export all types, to avoid warnings.
print_type_exports(Module) ->
    to_io_list(["-export_type([",
                separate([print_type_export(M) || M <- Module:get_msg_defs()],
                         ",\n              "),
                "]).\n\n"]).

rpcs(Module) ->
    lists:flatten([begin
                       {_, Rs} = Module:get_service_def(S),
                       Rs
                   end || S <- Module:get_service_names()]).
 
print_messages(Module) -> 
    to_io_list([print_message(M) || M <- Module:get_msg_defs()]).

print_decoder(Module) ->
    to_io_list(["-spec decoder() -> module().\n",
                "%% The module (generated by gpb) used to encode and decode protobuf\n"
                "%% messages.\n",
                "decoder() -> ", Module, ".\n\n"]).

print_services(Module, Role) ->
    to_io_list([print_service(Module:get_service_def(S), Role) 
                || S <- Module:get_service_names()]).

print_service({{service, Name}, RPCs}, Role) ->
    ["%% RPCs for service ", Name, "\n\n",
     print_rpcs(RPCs, Name, Role)].

print_rpcs(RPCs, _Servcie, server) ->
    [print_rpc(R) || R <- RPCs];
print_rpcs(RPCs, Service, client) ->
    [print_rpc_call(R, Service) || #{input_stream := false,
                                     output_stream := false} = R <- RPCs].

print_rpc(#{input_stream := InStream, output_stream := OutStream} = Rpc) ->
    case {InStream, OutStream} of 
        {false, false} ->
            print_unary_rpc(Rpc);
        {false, true} -> 
            print_server_to_client_streaming_rpc(Rpc);
        {true, false} ->
            print_client_to_server_streaming_rpc(Rpc);
        {true, true} ->
            print_bidirectional_streaming_rpc(Rpc)
    end.
 
print_unary_rpc(#{name := Name,  
                  output := Output} = Rpc) ->
    [print_spec_call(Rpc),
     "    {", Output, "(), grpc:stream()} | grpc:error_response().\n",
     "%% This is a unary RPC\n",
     Name, "(_Message, Stream, _State) ->\n",
     "    {#{}, Stream}.\n\n"].

print_server_to_client_streaming_rpc(#{name := Name, 
                                       output := Output} = Rpc) ->
    [print_spec_call(Rpc),
     "    {[", Output, "()], grpc:stream()} | grpc:error_response().\n",
     "%% This is a server-to-client streaming RPC\n",
     Name, "(_Message, Stream, _State) ->\n",
     "    {[#{}], Stream}.\n\n"].

print_client_to_server_streaming_rpc(#{name := Name, 
                                       input := Input, 
                                       output := Output}) ->
    ["-spec ", Name, 
     "(Message::", Input, "() | eof, Stream::grpc:stream(), State::any()) ->\n",
     "    {continue, grpc:stream(), any()} |\n",
     "    {", Output, "(), grpc:stream()} |\n",
     "    grpc:error_response().\n",
     "%% This is a client-to-server streaming RPC. After the client has sent the last message\n",
     "%% this function will be called a final time with 'eof' as the first argument. This last\n",
     "%% invocation must return the response message.\n",
     Name, "(_Message, Stream, State) ->\n",
     "    {continue, Stream, State}.\n\n"].

print_bidirectional_streaming_rpc(#{name := Name, 
                                    input := Input, 
                                    output := Output}) ->
    ["-spec ", Name, 
     "(Message::", Input, "() | eof, Stream::grpc:stream(), State::any()) ->\n",
     "    {[", Output, "()], grpc:stream(), any()} |\n",
     "    {[", Output, "()], grpc:stream()} |\n",
     "    grpc:error_response().\n",
     "%% This is a bidirectional streaming RPC. If the client terminates the stream\n",
     "%% this function will be called a final time with 'eof' as the first argument.\n", 
     Name, "(_Message, Stream, State) ->\n",
     "    {[], Stream, State}.\n\n"].
 
%% the client side, only for non-streaming rpcs.
print_rpc_call(#{name := Name,  
                 input := Input,
                 output := Output} = _Rpc, Service) ->
    ["-spec ", Name, "(\n",
     "        Connection::grpc_client:connection(),\n", 
     "        Message::", Input, "(),\n",
     "        Options::[grpc_client:stream_option() |\n",
     "                  {timeout, timeout()}]) ->\n",
     "        grpc_client:unary_response(", Output, "()).\n",
     "%% This is a unary RPC\n",
     Name, "(Connection, Message, Options) ->\n",
     "    grpc_client:unary(Connection, Message,\n"
     "                      ", Service, ", ", Name, ",\n",
     "                       decoder(), Options).\n\n"].

print_spec_call(#{name := Name, input := Input}) ->
    ["-spec ", Name, 
     "(Message::", Input, "(), Stream::grpc:stream(), State::any()) ->\n"].

print_message({{msg, Name}, Fields}) -> 
    ["-type ", Name, "() ::\n    #{", print_fields(Fields), "}.\n\n"];
print_message({{enum, Name}, Options}) -> 
    ["-type ", Name, "() ::\n    ", print_options(Options), ".\n\n"].

print_type_export({{msg, Name}, _Fields}) -> 
    [Name, "/0"];
print_type_export({{enum, Name}, _Values}) -> 
    [Name, "/0"].

print_fields(Fields) -> 
    separate([print_field(F) || F <- Fields], ",\n      ").

print_options(Options) -> 
    separate([V || {V, _} <- Options], " |\n    ").

print_field(#{name := Name, type := Type, occurrence := Occurrence}) ->
    [Name, 
     case {Occurrence, Type} of 
         {optional, _} -> " => ";
         {required, _} -> " := ";
         %% Maps are marked as 'repeated' by gpb, but in fact they 
         %% have 1 value (a single map, not a list).
         {repeated, {map, _, _}}  -> " => ";
         {repeated, _}  -> " => ["
     end, 
     print_type(Type),
     case {Occurrence, Type} of 
         {repeated, {map, _, _}}  -> "";
         {repeated, _}  -> "]";
         _ -> ""
     end];
print_field(#{name := Name, fields := Alternatives}) ->
    [Name, " =>\n          ",
     separate([print_alternative(F) || F <- Alternatives], " |\n          ")].

print_alternative(#{name := Name, type := Type}) ->
    ["{", Name, ", ", print_type(Type), "}"].

print_type(int32) -> "integer()";
print_type(int64) -> "integer()";
print_type(uint32) -> "integer()";
print_type(uint64) -> "integer()";
print_type(sint32) -> "integer()";
print_type(sint64) -> "integer()";
print_type(fixed32) -> "integer()";
print_type(fixed64) -> "integer()";
print_type(sfixed32) -> "integer()";
print_type(sfixed64) -> "integer()";
print_type(string) -> "string()";
print_type(bool) -> "boolean()";
print_type(bytes) -> "binary()";
print_type(double) -> "float() | infinity | '-infinity' | nan";
print_type(float) -> "float() | infinity | '-infinity' | nan";
print_type({enum, Enum}) -> [Enum, "() | integer()"];
print_type({map, KeyType, ValueType}) -> 
    ["#{", print_type(KeyType), " => ", print_type(ValueType), "}"];
print_type({msg, MsgName}) -> [MsgName, "()"].

separate([], _) ->
    [];
separate(L, With) ->
    [Last | Rest] = lists:reverse(L),
    separate(Rest, With, [Last]).

separate([], _, Acc) ->
    Acc;
separate([I | T], With, Acc) ->
    separate(T, With, [I, With | Acc]).


to_io_list(L) ->
    print2(lists:flatten(L)).

print2(L) ->
    [lists:flatten([print3(C) || C <- L])].

print3(C) when is_atom(C) ->
    io_lib:format("~p", [C]);
print3(C) ->
    C.

string_to_atom(S) when is_list(S) ->
    io_lib:format("~p", [list_to_atom(S)]).

